1
2
3
4
5
6
7
8 package org.opensciencegrid.authz.service;
9
10
11 import java.util.ArrayList;
12 import java.util.Iterator;
13 import java.util.Calendar;
14 import java.rmi.RemoteException;
15 import org.glite.security.SecurityContext;
16 import org.glite.security.util.axis.InitSecurityContext;
17
18 import org.opensaml.v1_0_1.XML;
19 import org.opensaml.v1_0_1.QName;
20 import org.opensaml.v1_0_1.SAMLDecision;
21 import org.opensaml.v1_0_1.SAMLException;
22 import org.opensaml.v1_0_1.SAMLSubject;
23 import org.opensaml.v1_0_1.SAMLRequest;
24 import org.opensaml.v1_0_1.SAMLAssertion;
25 import org.opensaml.v1_0_1.SAMLResponse;
26 import org.opensaml.v1_0_1.SAMLAuthorizationDecisionQuery;
27 import org.opensaml.v1_0_1.SAMLAuthorizationDecisionStatement;
28 import org.opensaml.v1_0_1.SAMLAttribute;
29 import org.opensaml.v1_0_1.SAMLAttributeStatement;
30 import org.opensaml.v1_0_1.SAMLAction;
31
32 import org.opensciencegrid.authz.saml.XACMLObligation;
33 import org.opensciencegrid.authz.saml.ObligatedAuthorizationDecisionStatement;
34 import org.opensciencegrid.authz.saml.OSGXML;
35 import org.opensciencegrid.authz.saml.SAMLExtensionInit;
36 import org.opensciencegrid.authz.saml.SAMLUtil;
37
38 import org.opensciencegrid.authz.stubs.SAMLRequestPortType;
39 import org.opensciencegrid.authz.stubs.SAMLRequestType;
40 import org.opensciencegrid.authz.stubs.SAMLResponseType;
41
42 import org.opensciencegrid.authz.common.OSGAuthorizationConstants;
43
44 import org.apache.log4j.Category;
45 import org.apache.axis.message.MessageElement;
46 import org.w3c.dom.Element;
47
48 /***
49 * Common implementation for a SAML authorization service: parses the requests,
50 * and performs the authorization through an abstract method, to be implemented
51 * by the AuthZ service.
52 *
53 * Status: untested
54 *
55 * TODO: retrieving the caller identity from the trustmanager
56 *
57 * @author Markus Lorch, Gabriele Carcassi, John Weigand
58 */
59
60 public abstract class SAMLAuthZServiceBase implements SAMLRequestPortType {
61
62
63 /*** This inner class is used as return value from the authorize method */
64
65 protected class AuthzDecision {
66
67 /*** decision may have the following values
68 * org.opensaml.v1_0_1.SAMLDecision.INDETERMINATE, SAMLDecision.DENY, SAMLDecision.PERMIT,
69 * is initialized to SAMLDecision.INDETERMINATE
70 */
71 public String decision = SAMLDecision.INDETERMINATE;
72
73 /*** set of actions authorized by the authorize method */
74 public ArrayList actions;
75
76 /*** set of obligations imposed on the response by the authorize method */
77 public ArrayList obligations;
78
79 /*** holds the issuer name of the authorization decision to be embedded in the SAMLResponse */
80 public String issuer;
81
82 /*** method to print the contents for debugging */
83 public String toString() {
84 String s;
85 s = "Decision: "+decision;
86 s = s.concat(", Actions: ");
87 if(actions==null ) s=s.concat("null");
88 else if(actions.isEmpty()) s= s.concat("empty");
89 else
90 for(int i=0;i<actions.size();i++) {
91 Object x = actions.get(i);
92 if (x instanceof SAMLAction) {
93 SAMLAction a = (SAMLAction) x;
94 s=s.concat(a.getNamespace()+" "+a.getData()+" | ");
95 }
96 else
97 s= s.concat(actions.get(i).toString()+" | ");
98 }
99 s=s.concat(", Obligations: ");
100 if(obligations==null) s=s.concat("null");
101 else if(obligations.isEmpty()) s= s.concat("empty");
102 else
103 for(int i=0;i<obligations.size();i++) {
104 Object x = obligations.get(i);
105 if (x instanceof XACMLObligation) {
106 XACMLObligation o = (XACMLObligation) x;
107 s=s.concat(o.getObligationId()+" "+o.getAttributeId()+"="+o.getValue())+" | ";
108 }
109 else
110 s= s.concat("Unsupported Obligation | ");
111 }
112 s = s.concat(", Issuer: "+issuer);
113 return s;
114 }
115
116 }
117
118 /*** the Fully Qualified Attribute Name (FQAN) = the VOMS VO-membership/role attribute */
119 protected class FQAN {
120 String issuer;
121 String data;
122 }
123
124
125
126
127 /*** this service can only respond with a SAML AuthorizationDecisionStatement or an
128 ObligatedAuthorizationDecisionStatement (this form is an OSG SAML Extension) */
129
130 private static final QName AUTHZ_DECISION_STMT = new QName(XML.SAML_NS,"AuthorizationDecisionStatement");
131 private static final QName OBLIG_DECISION_STMT = new QName(OSGXML.SAML_EXT_NS,"ObligatedAuthorizationDecisionStatement");
132
133 /*** determins for how long (in minutes) should issued assertions be valid */
134 private static final int ASSERTION_VALIDITY_IN_MINUTES = 10;
135
136 /*** Log4Java logger */
137 static Category log = Category.getInstance(SAMLAuthZServiceBase.class.getName() );
138
139
140
141
142
143
144
145 /*** Performs the authorization of the request.
146 *
147 * The abstract authorize method must be implemented by an a concrete subclass that implements an authorization
148 * service. At a minimum the implementation should:
149 * - evaluate subject, resource and requested actions
150 * - return SAMLDecision.DENY, SAMLDecision.PERMIT, or SAMLDecision.INDETERMINATE
151 * if a decision could not be reached (e.g. service not authoritative for resource)
152 * - set member variable responseIssuer
153 * - set member variable responseActions (authorized actions if Decision=Permit, otherwise
154 * a copy of the requested actions)
155 * in addition it may
156 * - evaluate presented subject evidence
157 * (e.g. VOMS FQAN, other privilege attributes, or certificate chain)
158 * - create XACMLObligation objects and add tehm to the
159 * responseObligations member variable
160 */
161 protected abstract AuthzDecision authorize(SAMLSubject subject, String resource, Iterator actions, Iterator evidence)
162 throws SAMLException;
163
164
165 /*** Main function, recives SAMLRequest and response with a SAMLResponse
166 * 1. parse request, can request be serviced by this code?
167 * 2. parse authorization query
168 * 3. call authorize method implementation of concrete subclass, which will return decision object
169 * 4. verify and process authorization response from authorize method
170 * 5. create and return SAML response
171 * if obligations were provided return ObligatedAuthorizationDecisionStatement
172 * else return a standard AuthorizationDecisionStatement with the appropriate decision
173 */
174
175 public SAMLResponseType SAMLRequest(SAMLRequestType samlRequestType) throws java.rmi.RemoteException {
176
177 /*** the SAMLRequest we are processing */
178 SAMLRequest request = null;
179
180 /*** the originator of the request */
181 String requestIssuer = null;
182
183 /*** the authorization decision query from the saml request */
184 SAMLAuthorizationDecisionQuery authzQuery = null;
185
186 /*** true if we can use ObligatedAuthorizationDecisionStatement to respond */
187 boolean respondWithObligAuthzStmt = false;
188
189 /*** the AuthzDecision object is used to hold the return values set by the
190 implementation of the abstract authorize method */
191 AuthzDecision response = null;
192
193 /*** the set of samlStatements that hold the authorization decision - typically one */
194 ArrayList decisionStmts = null;
195
196 /*** the set of assertions that hold the decisionStmts - typically one*/
197 ArrayList responseAssertions = null;
198
199 /*** the generated samlResponse */
200 SAMLResponseType samlResponse = null;
201
202
203
204 try {
205
206
207 SAMLExtensionInit.init();
208
209
210
211
212
213
214 requestIssuer = SecurityUtil.retrieveClientDN();
215 log.debug("Processing incoming SAMLRequest from " + requestIssuer);
216
217
218
219
220
221
222 request = processSAMLRequest(samlRequestType);
223
224
225 respondWithObligAuthzStmt = checkRespondWith(request);
226
227
228 authzQuery = extractAuthorizationDecisionQuery(request);
229
230 if(authzQuery!=null) log.debug("Extracted authorization decision query");
231
232
233
234 log.debug("Calling implementation of authorize method in the authoirzation service subclass");
235
236 response = authorize( authzQuery.getSubject(),
237 authzQuery.getResource(),
238 authzQuery.getActions(),
239 authzQuery.getEvidence()
240 );
241 log.debug("Authorize method returned:"+ response.toString());
242
243
244
245
246
247 if (response.decision !=null &&
248 ( response.decision.equals(SAMLDecision.PERMIT) ||
249 response.decision.equals(SAMLDecision.DENY) ||
250 response.decision.equals(SAMLDecision.INDETERMINATE) )
251 ) {
252
253 }
254 else {
255
256 throw new Exception("Authorize method did not return valid decision value");
257 }
258
259 if (response.actions == null || response.actions.isEmpty())
260 throw new Exception("Authorize method did not return any actions");
261
262 if (response.issuer == null || response.issuer.length() ==0)
263 throw new Exception("Authorize method did not return issuer value");
264
265
266
267
268
269 if( (response.obligations!=null)
270 && (!response.obligations.isEmpty())
271 && (!respondWithObligAuthzStmt) ) {
272
273 response.obligations = null;
274 log.warn("Response requires obligations, but the set of client-requested response formats did not include ObligatedAuthoirzationDecisionStatement");
275 log.warn("Must return INDETERMINATE, as obligations cannot be conveyed with standard response");
276 response.decision = SAMLDecision.INDETERMINATE;
277
278 }
279
280
281
282
283
284 log.debug("returning AuthorizationDecisionStatement with decision= "+ response.decision);
285
286 decisionStmts = createAuthzDecisionStmt(authzQuery, response.decision, response.actions, response.obligations);
287
288 responseAssertions = createAssertions(response.issuer, decisionStmts);
289
290 samlResponse = createSAMLResponse( request.getId(),
291 requestIssuer,
292 responseAssertions,
293 null
294 );
295
296 log.debug("Returning response");
297
298 return samlResponse;
299
300 } catch (Exception e) {
301
302
303
304 log.error("ABORT: "+e);
305 log.debug("Exception trace: ",e);
306 try {
307 log.debug("Attempting to construct a SAML AuthoirzationDecisionStatement with decision = INDETERMINATE");
308 if (response==null) response = new AuthzDecision();
309 if (response.actions == null) {
310 response.actions = new ArrayList(1);
311 log.debug("don't have actions set so we will duplicate requested actions");
312 Iterator ai = authzQuery.getActions();
313 while(ai.hasNext()) response.actions.add(ai.next());
314 }
315 if (response.issuer == null) response.issuer="Unknown";
316 decisionStmts = createAuthzDecisionStmt(authzQuery, SAMLDecision.INDETERMINATE, response.actions, null);
317 responseAssertions = createAssertions(response.issuer, decisionStmts);
318 samlResponse = createSAMLResponse( request.getId(),
319 requestIssuer,
320 responseAssertions,
321 null
322 );
323 log.error("Responding with SAML AuthorizationDecisionSatement with decision = INDETERMINATE");
324 return samlResponse;
325
326 } catch (Exception samlExcp) {
327 log.error("Unable to create a SAML AuthorizationDecisionStatement with decision = INDETERMINATE");
328 log.error("Caught exception was: "+samlExcp,samlExcp);
329 try {
330 SAMLException reportedException = new SAMLException(new QName(XML.SAMLP_NS, "RequestDenied"),
331 "Please contact the system administrator of this authorization service for more information");
332 samlResponse = createSAMLResponse(request.getId(), requestIssuer, null, reportedException);
333 log.error("Responding with SAML-Exception response: RequestDenied");
334 return samlResponse;
335 }
336 catch (Exception samlExcp2) {
337
338
339 log.error("Unable to respond to request");
340 log.error("Caught exception was: "+samlExcp,samlExcp);
341 throw new java.rmi.RemoteException("Service unable to respond to request!");
342 }
343
344 }
345 }
346
347
348 }
349
350
351
352
353 /*** Parses and returns the SAMLRequest in the RequestType
354 * throws RemoteException or SAMLException if problems occur
355 */
356 private SAMLRequest processSAMLRequest(SAMLRequestType requestType)
357 throws RemoteException, SAMLException {
358
359 SAMLRequest returnValue;
360
361
362
363
364 log.debug("Validating request type");
365 if (requestType == null) {
366 throw new RemoteException("Received a null request");
367 }
368
369
370
371
372
373 log.debug("Extracting SAMLRequest");
374 try {
375 MessageElement[] msgElement = requestType.get_any();
376 returnValue = new SAMLRequest(msgElement[0].getAsDOM());
377 if (returnValue == null) {
378 throw new RemoteException("SAMLRequest returned null");
379 }
380 } catch (Exception exp) {
381 throw new RemoteException("Error extracting SAML Request object: "+exp);
382 }
383
384 return returnValue;
385
386 }
387
388
389
390 /*** checks the respondWith request by the client, returns true
391 * if we can respond with an ObligatedAuthorizationDecisionStatement
392 * Note: If no respondWith is present, then true is also returned
393 * throws RemoteException or SAMLException if problems occur or
394 * if a SAML AuthorizationDecisionStatement is not supporte by
395 * the client
396 */
397 private boolean checkRespondWith(SAMLRequest request)
398 throws RemoteException, SAMLException {
399
400
401
402
403
404 boolean authzDecisionStmtFound = false;
405 boolean obligAuthzDecisionStmtFound = false;
406 Iterator respondWiths = request.getRespondWiths();
407
408 if (respondWiths == null ) {
409 log.debug("No respondWith elements found in request, enabling ObligatedAuthorizationDecisionStatement");
410 obligAuthzDecisionStmtFound = true;
411 } else {
412
413
414
415
416 while (respondWiths.hasNext()) {
417 QName respondWith = (QName)respondWiths.next();
418
419 if (respondWith.equals(this.AUTHZ_DECISION_STMT) ) {
420 authzDecisionStmtFound = true;
421 log.debug("Supported respondWith: "+respondWith.toString());
422 }
423 else if (respondWith.equals(this.OBLIG_DECISION_STMT) ) {
424 obligAuthzDecisionStmtFound = true;
425 log.debug("Supported respondWith: "+respondWith.toString());
426 }
427 else {
428 log.debug("Found unrecognized respondWith: "+respondWith.toString());
429 }
430 }
431
432 if (!authzDecisionStmtFound) {
433 log.debug("Standard SAML AuthorizationDecisionStatement is not supported by client, aborting");
434 throw new java.rmi.RemoteException("Standard SAML AuthorizationDecisionStatement is not supported by client.");
435 }
436
437 }
438
439 return obligAuthzDecisionStmtFound;
440
441 }
442
443
444
445
446 /*** Extracts an authorization decision query from the request
447 * throws RemoteException or SAMLException if problems occur
448 */
449 private SAMLAuthorizationDecisionQuery extractAuthorizationDecisionQuery(SAMLRequest request)
450 throws RemoteException, SAMLException {
451
452 Object obj = request.getQuery();
453 if (obj instanceof SAMLAuthorizationDecisionQuery ) {
454 log.debug("FOUND SAMLAuthorizationDecisionQuery: "+obj);
455 return (SAMLAuthorizationDecisionQuery) obj;
456 } else {
457 log.error("Request not supported: "+obj);
458 throw new java.rmi.RemoteException("Request not supported.");
459 }
460
461 }
462
463
464
465 /*** Returns the Distinguished Name of a SAMLSubject.
466 * Needs: X.509 format validation
467 */
468 private String getRequestUser(SAMLSubject subject)
469 throws SAMLException {
470 String subjectDN = subject.getName().getName();
471 return subjectDN;
472 }
473
474
475
476 /*** Creates an ArrayList with a either a standard SAML AuthorizationDecisionStatement
477 * or an ObligatedAuthorizationDecisionStatement based on the
478 * value of the responseObligations parameter
479 */
480 private ArrayList createAuthzDecisionStmt(SAMLAuthorizationDecisionQuery query,
481 String decision,
482 ArrayList actions,
483 ArrayList obligations)
484 throws SAMLException {
485
486 ArrayList samlStmts = new ArrayList(1);
487 Object stmt;
488
489 if( (obligations!=null) && (!obligations.isEmpty() ) )
490 stmt = new ObligatedAuthorizationDecisionStatement(query.getSubject(),
491 query.getResource(),
492 decision,
493 actions,
494 null,
495 obligations);
496 else
497 stmt = new SAMLAuthorizationDecisionStatement(query.getSubject(),
498 query.getResource(),
499 decision,
500 actions,
501 null);
502
503 samlStmts.add(stmt);
504
505 return samlStmts;
506
507 }
508
509
510
511
512
513
514 /*** Creates an arraylist of SAMLAssertions that hold a single
515 * assertion with the provided SAMLStatements and a validity
516 * time constraint from now for ASSERTION_VALIDITY_IN_MINUTES
517 * minutes
518 */
519 private ArrayList createAssertions(String issuer, ArrayList stmts)
520 throws SAMLException {
521
522 ArrayList assertions = new ArrayList(1);
523 SAMLAssertion samlAssertion;
524
525 Calendar notBefore = Calendar.getInstance();
526 Calendar notAfter = Calendar.getInstance();
527 notAfter.add(Calendar.MINUTE, this.ASSERTION_VALIDITY_IN_MINUTES);
528
529 samlAssertion = new SAMLAssertion(issuer,
530 notBefore.getTime(),
531 notAfter.getTime(),
532 null,
533 null,
534 stmts);
535
536 assertions.add(samlAssertion);
537
538 return assertions;
539
540 }
541
542
543
544 /*** Creates the SAMLResponseType holding the assertions
545 */
546 private SAMLResponseType createSAMLResponse(String inResponseTo,
547 String recipient,
548 ArrayList assertions,
549 SAMLException samlException)
550 throws SAMLException {
551
552 SAMLResponseType samlResponse = null;
553
554 SAMLResponse response = new SAMLResponse(inResponseTo, recipient, assertions, samlException);
555
556 log.debug("Created SAMLResponse: "+response);
557
558 samlResponse = new SAMLResponseType();
559
560 samlResponse.set_any(new MessageElement[] { new MessageElement((Element)response.toDOM())});
561
562 return samlResponse;
563
564 }
565
566
567
568 /*** checks if a specific SAMLAttributeStatement holds a FQAN, and
569 * returns that FQAN attribute in form of a string
570 * returns null if no FQAN (string) attribute could be located
571 */
572
573 protected String getFQAN(SAMLAttributeStatement stmt) throws SAMLException {
574
575 String fqan = null;
576
577 Iterator attributeIterator = stmt.getAttributes();
578
579 while (attributeIterator.hasNext()) {
580 Object attrib = attributeIterator.next();
581 if ( ! (attrib instanceof SAMLAttribute) ) {
582 continue;
583 }
584 else {
585 String attrName = ((SAMLAttribute)attrib).getName();
586 String attrNamespace = ((SAMLAttribute)attrib).getNamespace();
587 log.debug("Found Evidence Attribute: "+attrName+ " with namespace: "+attrNamespace);
588
589 if ( attrName.equals("FQAN") && attrNamespace.equals(OSGAuthorizationConstants.AUTHZ_NS) ) {
590 fqan = getAttributeValue( (SAMLAttribute) attrib);
591 }
592 }
593 }
594
595 if(fqan!=null) log.debug("Found FQAN Attribute: "+fqan);
596 else log.warn("Found unsupported FQAN Attribute (it had no string value!!!)");
597 return fqan;
598
599 }
600
601
602 /*** Searches the Evidence elements for FQAN attributes.
603 * Returns the first FQAN found that matches the querySubject
604 * These are what GUMS needs to perform the authorization and
605 * determine the username.
606 */
607
608 protected FQAN findFQANinSubjectEvidence(Iterator evidenceIterator, SAMLSubject querySubject)
609 throws SAMLException {
610
611 FQAN fqan = new FQAN();
612
613 while (evidenceIterator.hasNext()) {
614 Object obj = evidenceIterator.next();
615 if (obj instanceof SAMLAssertion) {
616
617 fqan.issuer = ( (SAMLAssertion) obj).getIssuer();
618
619 Iterator evidenceStatementIterator = ((SAMLAssertion)obj).getStatements();
620 while (evidenceStatementIterator.hasNext()) {
621 Object stmt = evidenceStatementIterator.next();
622 if (stmt instanceof SAMLAttributeStatement) {
623 if (SAMLUtil.samlSubjectMatch(querySubject, ((SAMLAttributeStatement)stmt).getSubject() ) ) {
624
625 fqan.data=getFQAN((SAMLAttributeStatement)stmt);
626
627 if(fqan.data!=null) {
628 return fqan;
629 }
630
631 }
632 }
633 }
634 }
635 }
636
637 return null;
638
639 }
640
641
642 /*** returns the subset of the requestedActions that are present in the
643 * permissibleActions parameter
644 */
645
646 protected ArrayList locatePermissibleActions(Iterator requestedActions, ArrayList permissibleActionsList) {
647
648 ArrayList permittedActions = new ArrayList(1);
649
650 log.debug("Locating permissible actions");
651
652
653 while (requestedActions.hasNext()) {
654 SAMLAction ra = (SAMLAction)requestedActions.next();
655
656 for(int i=0;i<permissibleActionsList.size();i++) {
657 SAMLAction pa = (SAMLAction)permissibleActionsList.get(i);
658
659
660
661 if(SAMLUtil.samlActionMatch(pa,ra) ) {
662 log.debug("requested action matched permissible action: "+ra);
663
664 permittedActions.add(ra);
665
666
667 permissibleActionsList.remove(i);
668
669 break;
670 }
671
672 }
673
674 }
675
676
677 if (permittedActions.isEmpty() ) return null;
678
679 return permittedActions;
680
681 }
682
683
684
685 /*** Returns the first string value of an attribute,
686 * returns null if not string attribute found
687 */
688 private String getAttributeValue(SAMLAttribute attrib)
689 throws SAMLException {
690
691 String value = null;
692 Iterator attributeValues = ((SAMLAttribute)attrib).getValues();
693 while (attributeValues.hasNext()) {
694 Object attributeValue = attributeValues.next();
695 if( ! (attributeValue instanceof String)) {
696 continue;
697 }
698 value = (String) attributeValue;
699 break;
700 }
701
702 return value;
703 }
704
705
706 }